|
Server : Apache System : Linux server.mata-lashes.com 3.10.0-1160.90.1.el7.x86_64 #1 SMP Thu May 4 15:21:22 UTC 2023 x86_64 User : matalashes ( 1004) PHP Version : 8.1.29 Disable Function : NONE Directory : /usr/src/cloud-init/ |
Upload File : |
"""Global conftest.py
This conftest is used for unit tests in ``cloudinit/`` and ``tests/unittests/``
as well as the integration tests in ``tests/integration_tests/``.
Any imports that are performed at the top-level here must be installed wherever
any of these tests run: that is to say, they must be listed in
``integration-requirements.txt`` and in ``test-requirements.txt``.
"""
from unittest import mock
import pytest
from cloudinit import helpers, subp, util
class _FixtureUtils:
"""A namespace for fixture helper functions, used by fixture_utils.
These helper functions are all defined as staticmethods so they are
effectively functions; they are defined in a class only to give us a
namespace so calling them can look like
``fixture_utils.fixture_util_function()`` in test code.
"""
@staticmethod
def closest_marker_args_or(request, marker_name: str, default):
"""Get the args for closest ``marker_name`` or return ``default``
:param request:
A pytest request, as passed to a fixture.
:param marker_name:
The name of the marker to look for
:param default:
The value to return if ``marker_name`` is not found.
:return:
The args for the closest ``marker_name`` marker, or ``default``
if no such marker is found.
"""
try:
marker = request.node.get_closest_marker(marker_name)
except AttributeError:
# Older versions of pytest don't have the new API
marker = request.node.get_marker(marker_name)
if marker is not None:
return marker.args
return default
@staticmethod
def closest_marker_first_arg_or(request, marker_name: str, default):
"""Get the first arg for closest ``marker_name`` or return ``default``
This is a convenience wrapper around closest_marker_args_or, see there
for full details.
"""
result = _FixtureUtils.closest_marker_args_or(
request, marker_name, [default]
)
if not result:
raise TypeError(
"Missing expected argument to {} marker".format(marker_name)
)
return result[0]
@pytest.fixture(autouse=True)
def disable_subp_usage(request, fixture_utils):
"""
Across all (pytest) tests, ensure that subp.subp is not invoked.
Note that this can only catch invocations where the ``subp`` module is
imported and ``subp.subp(...)`` is called. ``from cloudinit.subp import
subp`` imports happen before the patching here (or the CiTestCase
monkey-patching) happens, so are left untouched.
While ``disable_subp_usage`` unconditionally patches
``cloudinit.subp.subp``, any test-local patching will override this
patching (i.e. the mock created for that patch call will replace the mock
created by ``disable_subp_usage``), allowing tests to be written normally.
One important exception: if ``autospec=True`` is passed to such an
overriding patch call it will fail: autospeccing introspects the object
being patched and as ``subp.subp`` will always be a mock when that
autospeccing happens, the introspection fails. (The specific error is:
``TypeError: name must be a str, not a MagicMock``.)
To allow a particular test method or class to use ``subp.subp`` you can
mark it as such::
@pytest.mark.allow_all_subp
def test_whoami(self):
subp.subp(["whoami"])
To instead allow ``subp.subp`` usage for a specific command, you can use
the ``allow_subp_for`` mark::
@pytest.mark.allow_subp_for("bash")
def test_bash(self):
subp.subp(["bash"])
You can pass multiple commands as values; they will all be permitted::
@pytest.mark.allow_subp_for("bash", "whoami")
def test_several_things(self):
subp.subp(["bash"])
subp.subp(["whoami"])
This fixture (roughly) mirrors the functionality of
``CiTestCase.allowed_subp``. N.B. While autouse fixtures do affect
non-pytest tests, CiTestCase's ``allowed_subp`` does take precedence (and
we have ``TestDisableSubpUsageInTestSubclass`` to confirm that).
"""
allow_subp_for = fixture_utils.closest_marker_args_or(
request, "allow_subp_for", None
)
# Because the mark doesn't take arguments, `allow_all_subp` will be set to
# [] if the marker is present, so explicit None checks are required
allow_all_subp = fixture_utils.closest_marker_args_or(
request, "allow_all_subp", None
)
if allow_all_subp is not None and allow_subp_for is None:
# Only allow_all_subp specified, don't mock subp.subp
yield
return
if allow_all_subp is None and allow_subp_for is None:
# No marks, default behaviour; disallow all subp.subp usage
def side_effect(args, *other_args, **kwargs):
raise AssertionError("Unexpectedly used subp.subp")
elif allow_all_subp is not None and allow_subp_for is not None:
# Both marks, ambiguous request; raise an exception on all subp usage
def side_effect(args, *other_args, **kwargs):
raise AssertionError(
"Test marked both allow_all_subp and allow_subp_for: resolve"
" this either by modifying your test code, or by modifying"
" disable_subp_usage to handle precedence."
)
else:
# Look this up before our patch is in place, so we have access to
# the real implementation in side_effect
real_subp = subp.subp
def side_effect(args, *other_args, **kwargs):
cmd = args[0]
if cmd not in allow_subp_for:
raise AssertionError(
"Unexpectedly used subp.subp to call {} (allowed:"
" {})".format(cmd, ",".join(allow_subp_for))
)
return real_subp(args, *other_args, **kwargs)
with mock.patch("cloudinit.subp.subp", autospec=True) as m_subp:
m_subp.side_effect = side_effect
yield
@pytest.fixture(scope="session")
def fixture_utils():
"""Return a namespace containing fixture utility functions.
See :py:class:`_FixtureUtils` for further details."""
return _FixtureUtils
@pytest.fixture
def mocked_responses():
import responses as _responses
with _responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
yield rsps
@pytest.fixture
def paths(tmpdir):
"""
Return a helpers.Paths object configured to use a tmpdir.
(This uses the builtin tmpdir fixture.)
"""
dirs = {
"cloud_dir": tmpdir.mkdir("cloud_dir").strpath,
"run_dir": tmpdir.mkdir("run_dir").strpath,
}
return helpers.Paths(dirs)
@pytest.fixture(autouse=True, scope="session")
def monkeypatch_system_info():
def my_system_info():
return {
"platform": "invalid",
"system": "invalid",
"release": "invalid",
"python": "invalid",
"uname": ["invalid"] * 6,
"dist": ("Distro", "-1.1", "Codename"),
"variant": "ubuntu",
}
util.system_info = my_system_info